package com.wawayaya.bookreader.stroke;

import android.util.Log;

import com.wawayaya.bookreader.jni.ChnStrokeFiller;
import com.wawayaya.bookreader.bean.ShortPoint;
import  com.wawayaya.bookreader.bean.StrokeRecord;

import java.util.Vector;

/** class to implement seed-Filling (no GUI)
 * 
 * @author wankang
 * 2011/08/18
 */
public class FillStroke {
	/** string for log only
	 */
	private static final String TAG = FillStroke.class.getSimpleName();
	/** initial contours of one Chinese character
	 */
	private Vector<StrokeRecord> mList = null;
	/** maximum width of showing control
	 */
	private int mMaxWidth = 267;
	/** maximum height of showing control
	 */
	private int mMaxHeight = 267;
	/** 1-based contour index
	 */
	private int mNextContour = 1;
	/** bytes count needed by one row
	 */
	private int mWidthByte = 1;
	/** flag of filled, 1-bit for a point
	 */
	private byte[] mFillFlag = null;
	/** those points whose neighbor should be checked
	 */
	private Vector<ShortPoint> mFindList = null;
	/** result from ChnStrokeFiller, for complex stroke only
	 */
	private short[] mPtResult = null;
	/** -1: to query start point; >=0([0, 8]): to find from next point's neighbor
	 */
	private int mNextNeighbourIndex = -1;
	/** status: 0, normal filling; 1, contour end; 2, all end
	 */
	private int mResult = -1;
	/** filling style: 0, default style; 1, round(circle) style; 2, round style, with corner detection
	 */
	private int mStyle = 0;
	
	private boolean mComplexStroke = false;
	private IQueryStrokeGuide mqueryGuide = null;
	
	/** neighbor offset (x, y), default offset
	 */
	private static final int[] BLOCK9_XY = new int[]{-1, -1, 0, -1, 1, -1, -1, 0, 1, 0, -1, 1, 0, 1, 1, 1};
	/** neighbor offset (x, y), circle offset
	 */
	private static final int[] CIRCLE_XY = new int[]{0, -1, 1, 0, 0, 1, -1, 0, -1, -1, 1, -1, 1, 1, -1, 1,
		-1, -2, 0, -2, 1, -2, 2, -1, 2, 0, 2, 1, 1, 2, 0, 2, -1, 2, -2, 1, -2, 0, -1, -2};

	/** interface for complex stroke filling
	 */
	public interface IQueryStrokeGuide {
		/** callback to query guide points of contour middle line
		 * @param sChn code of Chinese character
		 * @param nStrokeIndex 1-based stroke index
		 * @return null, if fail
		 */
		public ShortPoint[] getStrokeGuide(String sChn, int nStrokeIndex);
	}
	
	public FillStroke(Vector<StrokeRecord> strokeList, int nWidth, int nHeight, int nStyle) {
		mMaxWidth = nWidth;
		mMaxHeight = nHeight;
		mWidthByte = (nWidth + 7) / 8;
		if (nStyle==0 || nStyle==1 || nStyle==2)
			mStyle = nStyle;
		else
			throw new IllegalArgumentException("unknown style");
		saveContour(strokeList);
	}
	
	public void enableQueryGuide(IQueryStrokeGuide fnCall) {
		mqueryGuide = fnCall;
	}
	
	/** to get some filling points
	 * 
	 * @param vsp to output some filling points
	 * @param nMax maximum count of output points
	 * @return -1, for arguments' error; 0, normal status; 1, contour end; 2, all contours end
	 */
	public int getNextFillGroup(Vector<ShortPoint> vsp, int nMax) {
		if (vsp == null || mList == null || mList.size() <= 0 || nMax <= 0)
			return -1;
		mResult = -1;
		vsp.ensureCapacity(nMax);
		for(int i=0; i<nMax; i++) {
			ShortPoint sp = getNextFillPoint();
			if (mResult == 0 && sp != null) {
				vsp.add(i, sp);
			} else {
				break;
			}
		}
		if (mResult == 1) {
			// rewind to next contour
			mNextContour++;
			// will query start point at the next time
			mNextNeighbourIndex = -1;
			// to clear flags
			mFillFlag = null;
			// may end all contour
			if (mNextContour > mList.size())
				return 2;
		}
		return mResult;
	}
	
	/** to return 0-based next contour index
	 * @return 0-based next contour index
	 */
	public int getNextContourIndex() {
		return (mNextContour - 1);
	}
	
	/** to query contour by given 0-based index
	 * @param nIndex the given 0-based index
	 * @return all contour points
	 */
	public final ShortPoint[] getContour(int nIndex) {
		if (mList == null)
			return null;
		if (nIndex<0 || nIndex>=mList.size())
			return null;
		StrokeRecord sr = mList.get(nIndex);
		if (sr == null)
			return null;
		return sr.mContourLine;
	}
	/** to free memory
	 */
	public void close() {
		if (mList != null) {
			int nSize = mList.size();
			for(int i=0; i<nSize; i++) {
				StrokeRecord sr = mList.get(i);
				if (sr != null) {
					sr.mContourLine = null;
					sr.mMiddleLine = null;
				}
			}
			mList.clear();
			mList = null;
		}
		mFillFlag = null;
		if (mFindList != null) {
			mFindList.clear();
			mFindList = null;
		}
		mPtResult = null;
	}
	/** to save stroke contour data(width/height is 267)
	 * 
	 * @param strokeList input data, data may need to scale
	 * @return .T., if succeed; .F., if fail.
	 */
	private boolean saveContour(Vector<StrokeRecord> strokeList) {
		if (mMaxWidth<=0 || mMaxHeight<=0)
			return false;
		if (mMaxWidth==267 && mMaxHeight==267) {
			mList = strokeList;
			return true;
		}
		mList = new Vector<StrokeRecord>();
		int nSize = strokeList.size();
		float fRatioX = mMaxWidth / 267.0f;
		float fRatioY = mMaxHeight / 267.0f;
		for(int i=0; i<nSize; i++) {
			StrokeRecord sr1 = strokeList.get(i);
			StrokeRecord sr2 = new StrokeRecord();
			sr2.mCode = sr1.mCode;
			sr2.mStrokeName = sr1.mStrokeName;
			sr2.mContourIndex = sr1.mContourIndex;
			sr2.mStartX = (int)(sr1.mStartX * fRatioX + 0.5f);
			sr2.mStartY = (int)(sr1.mStartY * fRatioY + 0.5f);
			sr2.mBuShou = sr1.mBuShou;
			sr2.mStrokeEnd = sr1.mStrokeEnd;
			sr2.mEndAngle = sr1.mEndAngle;
			// to clone contour outline
			sr2.mContourLine = clonePointList(sr1.mContourLine, fRatioX, fRatioY);
			if (mStyle < 2 || sr2.mStrokeName.length() <= 1) {
				// to skip midLine
				sr2.mMiddleLine = null;
			} else {
				// to clone midLine
				sr2.mMiddleLine = clonePointList(sr1.mMiddleLine, fRatioX, fRatioY);
			}
			mList.add(sr2);
		}
		return true;
	}
	
	private static ShortPoint[] clonePointList(final ShortPoint[] spList, float fRatioX, float fRatioY) {
		int nCount = spList.length;
		ShortPoint spLast = null;
		Vector<ShortPoint> vsp = new Vector<ShortPoint>();
		for(int j=0; j<nCount; j++) {
			final ShortPoint sp1 = spList[j];
			ShortPoint sp2 = new ShortPoint();
			sp2.x = (short)(sp1.x * fRatioX + 0.5f);
			sp2.y = (short)(sp1.y * fRatioY + 0.5f);
			// to merge points in the same horizontal line
			if (spLast!=null && spLast.y==sp2.y) {
				if (spLast.x == sp2.x)
					continue;
				int nCurSize = vsp.size();
				if (nCurSize>=2 && vsp.get(nCurSize-2).y==sp2.y) {
					spLast.x = sp2.x;
					continue;
				}
			}
			vsp.add(sp2);
			spLast = sp2;
		}
		nCount = vsp.size();
		ShortPoint[] spRes = new ShortPoint[nCount];
		vsp.copyInto(spRes);
		return spRes;
	}
	/** to get next point should be filled; null, if fail
	 * @return next point, null, if fail.
	 */
	private ShortPoint getNextFillPoint() {
		int nSize = mList.size();
		// invalidate 1-based index
		if (mNextContour <= 0 || mNextContour > nSize+1) {
			mResult = -1;
			return null;
		}
		// finished, empty result
		if (mNextContour == nSize+1) {
			mResult = 2;
			return null;
		}
		if (mNextNeighbourIndex >= 0) {
			// to query a point only
			if (mComplexStroke && mStyle==2) {
				ShortPoint sp;
				if (mPtResult != null) {
					int nMax = mPtResult.length / 2;
					if (mNextNeighbourIndex >= nMax) {
						mResult = 1;
						mPtResult = null;
						return null;
					}
					sp = new ShortPoint();
					sp.x = mPtResult[2 * mNextNeighbourIndex + 0];
					sp.y = mPtResult[2 * mNextNeighbourIndex + 1];
					mNextNeighbourIndex++;
				} else {
					int nMaxSize = (mFindList!=null ? mFindList.size() : 0);
					if (nMaxSize <= 0) {
						mResult = 1;
						return null;
					}
					sp = mFindList.get(0);
					mFindList.remove(0);
				}
				mResult = 0;
				return sp;
			}
			// to search neighbor points
			final int[] offset = (mStyle>0 ? CIRCLE_XY : BLOCK9_XY);
			final int nMaxNeighbour = offset.length / 2;
			ShortPoint spCheck = new ShortPoint();
			ShortPoint[] spList = mList.get(mNextContour-1).mContourLine;
			while(mFindList != null && mFindList.size() > 0) {
				ShortPoint sp0 = mFindList.get(0);
				for(int i=mNextNeighbourIndex; i<nMaxNeighbour; i++) {
					spCheck.x = (short)(sp0.x + offset[2*i + 0]);
					spCheck.y = (short)(sp0.y + offset[2*i + 1]);
					if (checkFlag(spCheck) || !IsPtInContour(spCheck, spList))
						continue;
					onInnerPointFound(spCheck);
					mNextNeighbourIndex = i + 1;
					mResult = 0;
					return spCheck;
				}
				mFindList.remove(0);
				// if style is 1, skip 4 inner points
				mNextNeighbourIndex = ((mStyle > 0) ? 4 : 0);
			}
		} else {
			// to query start point
			if (mFindList != null)
				mFindList.clear();
			else
				mFindList = new Vector<ShortPoint>();
			mComplexStroke = false;
			ShortPoint sp = null;
			StrokeRecord sr = mList.get(mNextContour-1);
			if (mStyle == 2) {
				// to skip processing for simple stroke
				int nChar = sr.mStrokeName.length();
				if (nChar > 1) {
					try{
						if (createPointSet(sr, mNextContour))
							mComplexStroke = true;
					}catch(Exception e){
						e.printStackTrace();
					}
				}
			}
			if (sp == null) {
				sp = new ShortPoint();
				sp.x = (short)sr.mStartX;
				sp.y = (short)sr.mStartY;
				onInnerPointFound(sp);
			}
			// will search neighbor points
			mNextNeighbourIndex = 0;
			mResult = 0;
			return sp;
		}
		mResult = 1;
		return null;
	}
	
	private boolean createPointSet(StrokeRecord sr, int nContourIndex) {
		if (mqueryGuide == null)
			return false;
		String sChn = sr.mCode;
		ShortPoint[] spGuide = mqueryGuide.getStrokeGuide(sChn, nContourIndex);
		if (spGuide == null || spGuide.length <= 0)
			return false;
		final ShortPoint[] spContour = sr.mContourLine;
		ChnStrokeFiller.initialize("");
		ChnStrokeFiller csf = new ChnStrokeFiller();
		if (csf.enableFill()) {
			csf.setSize(mMaxWidth, mMaxHeight);
			int nSize1 = spContour.length;
			short[] pn = new short[nSize1 * 2];
			for(int i=0; i<nSize1; i++) {
				pn[2*i+0] = spContour[i].x;
				pn[2*i+1] = spContour[i].y;
			}
			csf.setStrokeData(pn, null);
			int nSize2 = spGuide.length;
			short[] qn = new short[nSize2 * 2];
			float fX = mMaxWidth / 267.0f;
			float fY = mMaxHeight / 267.0f;
			for(int i=0; i<nSize2; i++) {
				qn[2 * i + 0] = (short)(spGuide[i].x * fX + 0.5f);
				qn[2 * i + 1] = (short)(spGuide[i].y * fY + 0.5f);
			}
			csf.setCacheData(qn);
			mPtResult = csf.getPoints(-1);
			csf.close();
		}
		return true;
	}

	private void onInnerPointFound(ShortPoint sp) {
		// set flag for this point being filled
		saveFlag(sp);
		// to append into find list
		if (mPtResult != null)
			return;
		ShortPoint sp1 = new ShortPoint();
		sp1.x = sp.x;
		sp1.y = sp.y;
		mFindList.add(sp1);
	}
	
	private void saveFlag(ShortPoint sp) {
		if (mFillFlag == null || mFillFlag.length <= 0) {
			int nMaxSize = mWidthByte * mMaxHeight;
			mFillFlag = new byte[nMaxSize];
			for(int i=0; i<nMaxSize; i++)
				mFillFlag[i] = 0;
		}
		if (sp.x<0 || sp.y<0 || sp.x>=mMaxWidth || sp.y>=mMaxHeight) {
			Log.d(TAG, "out of index");
			return;
		}
		mFillFlag[(sp.x / 8) + mWidthByte * sp.y] |= (1 << (sp.x % 8));
	}
	
	private boolean checkFlag(ShortPoint sp) {
		if (sp.x <0 || sp.y < 0 || sp.x >= mMaxWidth || sp.y >= mMaxHeight)
			return true;
		if (mFillFlag == null)
			return false;
		int n = (mFillFlag[(sp.x / 8) + mWidthByte * sp.y] & (1 << (sp.x % 8)));
		return (n != 0);
	}
	
	static boolean IsEqualPoint(ShortPoint p1, ShortPoint p2){
		if (p1 == null || p2 == null)
			return false;
		return (p1.x == p2.x) && (p1.y == p2.y);
	}

	public static boolean IsPtInContour(ShortPoint pt, final ShortPoint[] pts) {
	   	if (pts == null)
	   		return false;

	   	int nPts = pts.length;
	   	if (nPts >= 2) {
	   		ShortPoint p0 = pts[0];
	   		ShortPoint p1 = pts[nPts - 1];
	   		if ( IsEqualPoint(p0, p1 ) ) 
	   			nPts--;
	   	}
	   	if (nPts == 1)
	   		return IsEqualPoint(pt, pts[0]);

	   	// get the intersections from the line of pt and the contour made up of the pts
	   	ShortPoint p0 = pts[nPts-1];
	   	ShortPoint p1;
	   	int nCross=0;

	   	for(int i=0; i<nPts; i++, p0=p1)
	   	{
	   		p1 = pts[i];
	   		// fast check if pt line cross with p0 - p1 or not.
	   		if (p0.y == p1.y)
	   			continue;	// skip horizontal line, treat it as vertex
	   		int ymin = Math.min(p0.y, p1.y);
	   		int ymax = Math.max(p0.y, p1.y);
	   		if (pt.y < ymin || pt.y > ymax || pt.y == p1.y) //overlook p1�� for p1 will be p0 next time
	   			continue; //out of the cross scope
	   		// deal with the case of vertex cross, like pt.y==p0.y
	   		if (pt.y == p0.y) {
	   			if (pt.x == p0.x)
	   				return true;
	   			if (pt.x > p0.x)
	   				continue; //no need.
	   			
	   			int iPrev = (i - 2 + nPts) % nPts;
	   			ShortPoint pPrev = pts[iPrev];
	   			// previous is horizontal line, go forward for a vertex
	   			while (pPrev.y == p0.y) {
	   				iPrev = (iPrev - 1 + nPts) % nPts;
	   				pPrev = pts[iPrev];
	   			}

	   			if (pt.y > Math.min(p1.y, pPrev.y) && pt.y < Math.max(p1.y, pPrev.y))
	   				nCross++;

	   			continue;
	   		}
	   		// vertical line
	   		if (p0.x == p1.x) {
	   			if (pt.x == p0.x) //cross
	   				return true;
	   			else if(pt.x < p0.x) //cross
	   				nCross++;
	   			continue;
	   		}
	   		// diagonal: calculate to see if it cross or not.
	   		float fx = ( (p0.x - p1.x)*pt.y - (p0.x*p1.y - p1.x*p0.y) ) / (p0.y - p1.y);
	   		int x = (int)((fx > 0) ? (fx + 0.5f) : (fx - 0.5f));

	   		if (pt.x == x)
	   			return true;
	   		else if(pt.x < x)
	   			nCross++;
	   	}

	   	return (nCross & 1) != 0;
	}
}
